using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Cesium3DTilesSelection;
using Unity.Mathematics;
using System.Linq;
using EarthGeospatial;

public enum TilesetSource
{
    FromCesiumIon = 0,
    FromUrl = 1
}
public class Cesium3DTileset : MonoBehaviour
{
    public TilesetSource tilesetSource;
    public string Url;
    public string IonAssetID;
    public string IonAccessToken;


    public bool AlwaysIncludeTangents = false;
    public bool GenerateSmoothNormals;
    public bool EnableWaterMask;

    public Material material;
    public Material waterMaterial;

    public List<CesiumExclusionZone> ExclusionZones;

    public Tileset pTileset { get; private set; }


    public int MaximumSimultaneousTileLoads = 20;
    public Int64 MaximumCachedBytes = 256 * 1024 * 1024;

    int _lastTilesRendered;
    int _lastTilesLoadingLowPriority;
    int _lastTilesLoadingMediumPriority;


    int _lastTilesLoadingHighPriority;

    int _lastTilesVisited;
    int _lastCulledTilesVisited;
    int _lastTilesCulled;
    int _lastMaxDepthVisited;

    long _startTime;

    bool _captureMovieMode;
    bool _beforeMoviePreloadAncestors;
    bool _beforeMoviePreloadSiblings;


    int _beforeMovieLoadingDescendantLimit;
    bool _beforeMovieKeepWorldOriginNearCamera;

    List<Tile> _tilesToNoLongerRenderNextFrame;

    public CesiumGeoreference _georeference;
    public CesiumGeoreference Georeference
    {
        get { return _georeference; }
        set
        {
            this._georeference = value;
            this.InvalidateResolvedGeoreference();
            this.ResolveGeoreference();
        }
    }

    public CesiumGeoreference ResolveGeoreference()
    {
        if (this.ResolvedGeoreference != null)
        {
            return this.ResolvedGeoreference;
        }

        if (this._georeference)
        {
            this._georeference = this.Georeference;
        }
        else
        {
            this.ResolvedGeoreference = CesiumGeoreference.GetDefaultGeoreference(this);
        }

        Cesium3DTilesetRoot pRoot = GetComponent<Cesium3DTilesetRoot>();
        if (pRoot)
        {
            this.ResolvedGeoreference.OnGeoreferenceUpdated += pRoot.HandleGeoreferenceUpdated;
            // Update existing tile positions, if any.
            pRoot.HandleGeoreferenceUpdated();
        }

        return this.ResolvedGeoreference;
    }

    private void InvalidateResolvedGeoreference()
    {
        if (this.ResolvedGeoreference != null)
        {
            // this.ResolvedGeoreference->OnGeoreferenceUpdated.RemoveAll(this);
        }
        this.ResolvedGeoreference = null;
    }

    [System.NonSerialized]
    public CesiumGeoreference ResolvedGeoreference;


    private void LoadTileset()
    {
        TilesetOptions options = new TilesetOptions();
        options.contentOptions.generateMissingNormalsSmooth = this.GenerateSmoothNormals;
        options.contentOptions.enableWaterMask = this.EnableWaterMask;

        TilesetExternals externals = new TilesetExternals();
        externals.pPrepareRendererResources = new UnityResourcePreparer(this);

        switch (tilesetSource)
        {
            case TilesetSource.FromUrl:
                pTileset = new Tileset(externals, Url, options);
                break;
            case TilesetSource.FromCesiumIon:
                pTileset = new Tileset(externals, IonAssetID, IonAccessToken, options);
                break;
            default: break;
        }
    }

    private List<Camera> useCamera;
    // Start is called before the first frame update
    void Start()
    {
        useCamera = GameObject.FindObjectsOfType<Camera>().ToList();
        LoadTileset();
    }

    public class UnityResourcePreparer : IPrepareRendererResources
    {
        public Cesium3DTileset self;



        public UnityResourcePreparer(Cesium3DTileset self)
        {
            this.self = self;
        }

        public void attachRasterInMainThread(Tile tile,
         int overlayTextureCoordinateID, RasterOverlayTile rasterTile,
        object pMainThreadRendererResources,
        double2 translation, double2 scale)
        {
            TileContentLoadResult pContent = tile.pContent;
            if (pContent && pContent.model)
            {
                CesiumGltfComponent pGltfContent = tile.pRendererResources as CesiumGltfComponent;
                if (pGltfContent)
                {
                    pGltfContent.AttachRasterTile(
                        tile,
                        rasterTile,
                        pMainThreadRendererResources as Texture2D,
                        translation,
                        scale,
                        overlayTextureCoordinateID);
                }
            }
        }

        public void detachRasterInMainThread(Tile tile, int overlayTextureCoordinateID, RasterOverlayTile rasterTile, object pMainThreadRendererResources)
        {
            var pContent = tile.pContent;
            if (pContent && pContent.model)
            {
                var pGltfContent = tile.pRendererResources as CesiumGltfComponent;
                if (pGltfContent)
                {
                    pGltfContent.DetachRasterTile(tile, rasterTile, pMainThreadRendererResources as Texture2D);
                }
            }
        }

        public void free(Tile tile, object pLoadThreadResult, object pMainThreadResult)
        {
            Debug.Log("free not implement:");
            // throw new NotImplementedException();
        }

        public void freeRaster(RasterOverlayTile rasterTile, object pLoadThreadResult, object pMainThreadResult)
        {
            throw new NotImplementedException();
        }

        public object prepareInLoadThread(Gltf.Model model, double4x4 transform)
        {
            CreateModelOptions options = new CreateModelOptions();
            options.alwaysIncludeTangents = self.GetAlwaysIncludeTangents();
            return CesiumGltfComponent.CreateOffGameThread(model, transform, options);
        }

        public object prepareInMainThread(Tile tile, object pLoadThreadResult)
        {
            TileContentLoadResult pContent = tile.pContent;
            if (pContent && pContent.model)
            {
                Debug.Log("update:" + tile.ID.ToString() + " " + tile.pContent.model.extras["Cesium3DTiles_TileUrl"]);
                return CesiumGltfComponent.CreateOnGameThread(self, pLoadThreadResult,
                 self.GetCesiumTilesetToUnrealRelativeWorldTransform(), self.material, self.waterMaterial);
            }
            return null;
        }

        public object prepareRasterInLoadThread(Gltf.ImageCesium image)
        {
            return CesiumTextureUtility.loadTextureAnyThreadPart(image);
        }

        public object prepareRasterInMainThread(RasterOverlayTile rasterTile, object pLoadThreadResult)
        {
            return pLoadThreadResult;
        }
    }

    private bool GetAlwaysIncludeTangents()
    {
        return AlwaysIncludeTangents;
    }

    // Update is called once per frame
    void Update()
    {

        if (pTileset == null)
        {
            LoadTileset();
        }
        List<UnityCameraParameters> cameras = this.GetCameras();
        if (!cameras.Any())
        {
            return;
        }
        var pRoot = GetComponent<Cesium3DTilesetRoot>();
        double4x4 unityWorldToTileset = math.inverse(pRoot._tilesetToUnrealRelativeWorld);
        List<ViewState> frustums = new List<ViewState>();
        foreach (var camera in cameras)
        {
            frustums.Add(CreateViewStateFromViewParameters(camera, unityWorldToTileset));
        }
        ViewUpdateResult result = this._captureMovieMode ? pTileset.updateViewOffline(frustums) : pTileset.updateView(frustums);
        updateLastViewUpdateResultState(result);
        removeVisibleTilesFromList(this._tilesToNoLongerRenderNextFrame, result.tilesToRenderThisFrame);
        hideTilesToNoLongerRender(this._tilesToNoLongerRenderNextFrame);
        this._tilesToNoLongerRenderNextFrame = result.tilesToNoLongerRenderThisFrame;
        showTilesToRender(result.tilesToRenderThisFrame);
    }

    void showTilesToRender(List<Tile> tiles)
    {
        foreach (Tile pTile in tiles)
        {
            if (pTile.state != Tile.LoadState.Done)
            {
                continue;
            }

            if (isInExclusionZone(ExclusionZones, pTile))
            {
                continue;
            }

            //     // That looks like some reeeally entertaining debug session...:
            //     // const Cesium3DTilesSelection::TileID& id = pTile->getTileID();
            //     // const CesiumGeometry::QuadtreeTileID* pQuadtreeID =
            //     // std::get_if<CesiumGeometry::QuadtreeTileID>(&id); if (!pQuadtreeID ||
            //     // pQuadtreeID->level != 14 || pQuadtreeID->x != 5503 || pQuadtreeID->y !=
            //     // 11626) { 	continue;
            //     //}

            CesiumGltfComponent Gltf = (CesiumGltfComponent)(pTile.pRendererResources);
            if (!Gltf)
            {
                // When a tile does not have render resources (i.e. a glTF), then
                // the resources either have not yet been loaded or prepared,
                // or the tile is from an external tileset and does not directly
                // own renderable content. In both cases, the tile is ignored here.
                continue;
            }

            // applyActorCollisionSettings(BodyInstance, Gltf);

            if (Gltf.transform.parent == null)
            {

                // The AttachToComponent method is ridiculously complex,
                // so print a warning if attaching fails for some reason
                Gltf.transform.SetParent(transform);
                //   if (!attached) {
                //     FString tileIdString(
                //         Cesium3DTilesSelection::TileIdUtilities::createTileIdString(
                //             pTile->getTileID())
                //             .c_str());
                //     UE_LOG(
                //         LogCesium,
                //         Warning,
                //         TEXT("Tile %s could not be attached to root"),
                //         *tileIdString);
                //   }
            }

            if (!Gltf.gameObject.activeSelf)
            {
                Gltf.gameObject.SetActive(true);
            }
        }
    }

    private bool isInExclusionZone(List<CesiumExclusionZone> exclusionZones, Tile tile)
    {
        if (!exclusionZones.Any())
        {
            return false;
        }
        // Apparently, only tiles with bounding REGIONS are
        // checked for the exclusion...
        BoundingRegion pRegion = tile.boundingVolume.pBoundingRegion;
        if (!pRegion)
        {
            return false;
        }
        foreach (CesiumExclusionZone ExclusionZone in exclusionZones)
        {
            GlobeRectangle cgExclusionZone = GlobeRectangle.fromDegrees(
                       ExclusionZone.West,
                       ExclusionZone.South,
                       ExclusionZone.East,
                       ExclusionZone.North);
            if (cgExclusionZone.computeIntersection(pRegion.getRectangle()))
            {
                return true;
            }
        }
        return false;
    }


    /**
     * @brief Hides the visual representations of the given tiles.
     *
     * The visual representations (i.e. the `getRendererResources` of the
     * tiles) are assumed to be `UCesiumGltfComponent` instances that
     * are made invisible by this call.
     *
     * @param tiles The tiles to hide
     */
    void hideTilesToNoLongerRender(List<Tile> tiles)
    {
        if (tiles == null) return;
        foreach (Tile pTile in tiles)
        {
            if (pTile.state != Tile.LoadState.Done)
            {
                continue;
            }

            CesiumGltfComponent Gltf = (CesiumGltfComponent)(pTile.pRendererResources);
            if (Gltf && Gltf.gameObject.activeSelf)
            {
                Gltf.gameObject.SetActive(false);
            }
            else
            {
                // TODO: why is this happening?
                Debug.Log("Tile to no longer render does not have a visible Gltf");
            }
        }
    }


    void removeVisibleTilesFromList(List<Tile> list, List<Tile> visibleTiles)
    {
        if (list == null || !list.Any())
        {
            return;
        }
        foreach (var pTile in visibleTiles)
        {
            var it = list.Find(x => x == pTile);
            if (it != null)
            {
                list.Remove(it);
            }
        }
    }

    ViewState CreateViewStateFromViewParameters(UnityCameraParameters camera, double4x4 unityWorldToTileset)
    {
        double horizontalFieldOfView = camera.fieldOfViewDegrees * Mathf.Deg2Rad;
        double aspectRatio = camera.viewportSize.x / camera.viewportSize.y;
        double verticalFieldOfView = math.atan2(math.tan(horizontalFieldOfView * 0.5), aspectRatio) * 2.0;

        Vector3 direction = camera.rotation * (new Vector3(0.0f, 0.0f, 1.0f));
        Vector3 up = camera.rotation * (new Vector3(0.0f, 1.0f, 0.0f));

        double3 tilesetCameraLocation = (math.mul(unityWorldToTileset, new double4(camera.location.x, camera.location.y, camera.location.z, 1.0))).xyz;
        double3 tilesetCameraFront = math.normalize(math.mul(unityWorldToTileset, new double4(direction.x, direction.y, direction.z, 0.0)).xyz);
        double3 tilesetCameraUp = math.normalize(math.mul(unityWorldToTileset, new double4(up.x, up.y, up.z, 0.0)).xyz);

        return ViewState.create(
            tilesetCameraLocation,
            tilesetCameraFront,
            tilesetCameraUp,
            new double2(camera.viewportSize.x, camera.viewportSize.y),
            horizontalFieldOfView,
            verticalFieldOfView);
    }

    void updateLastViewUpdateResultState(ViewUpdateResult result)
    {
        if (result.tilesToRenderThisFrame.Count != this._lastTilesRendered ||
            result.tilesLoadingLowPriority != this._lastTilesLoadingLowPriority ||
            result.tilesLoadingMediumPriority !=
                this._lastTilesLoadingMediumPriority ||
            result.tilesLoadingHighPriority != this._lastTilesLoadingHighPriority ||
            result.tilesVisited != this._lastTilesVisited ||
            result.culledTilesVisited != this._lastCulledTilesVisited ||
            result.tilesCulled != this._lastTilesCulled ||
            result.maxDepthVisited != this._lastMaxDepthVisited)
        {

            this._lastTilesRendered = result.tilesToRenderThisFrame.Count;
            this._lastTilesLoadingLowPriority = result.tilesLoadingLowPriority;
            this._lastTilesLoadingMediumPriority = result.tilesLoadingMediumPriority;
            this._lastTilesLoadingHighPriority = result.tilesLoadingHighPriority;

            this._lastTilesVisited = result.tilesVisited;
            this._lastCulledTilesVisited = result.culledTilesVisited;
            this._lastTilesCulled = result.tilesCulled;
            this._lastMaxDepthVisited = result.maxDepthVisited;

            // Debug.Log($"{}: %d ms, Visited %d, Culled Visited %d, Rendered %d, Culled %d, Max Depth Visited: %d, Loading-Low %d, Loading-Medium %d, Loading-High %d",
            //     this.name,
            //     // (std::chrono::high_resolution_clock::now() - this._startTime).count() /
            //     //     1000000,
            //     result.tilesVisited,
            //     result.culledTilesVisited,
            //     result.tilesToRenderThisFrame.size(),
            //     result.tilesCulled,
            //     result.maxDepthVisited,
            //     result.tilesLoadingLowPriority,
            //     result.tilesLoadingMediumPriority,
            //     result.tilesLoadingHighPriority);
        }
    }

    public void updateTilesetOptionsFromProperties()
    {
        TilesetOptions options = this.pTileset.Options;
        options.maximumCachedBytes = this.MaximumCachedBytes;
    }
    internal void UpdateTransformFromCesium()
    {
        double4x4 CesiumToUnreal = this.GetCesiumTilesetToUnrealRelativeWorldTransform();
        var gltfComponents = GetComponentsInChildren<CesiumGltfComponent>();
        foreach (var pGltf in gltfComponents)
        {
            pGltf.UpdateTransformFromCesium(CesiumToUnreal);
        }
    }

    private double4x4 GetCesiumTilesetToUnrealRelativeWorldTransform()
    {
        var root = GetComponent<Cesium3DTilesetRoot>();
        return root._tilesetToUnrealRelativeWorld;
    }

    public struct UnityCameraParameters
    {
        public Vector2 viewportSize;
        public Vector3 location;
        public Quaternion rotation;
        public double fieldOfViewDegrees;
    };
    public List<UnityCameraParameters> GetCameras()
    {
        List<UnityCameraParameters> cameraParaList = new List<UnityCameraParameters>(useCamera.Count);
        for (var i = 0; i < useCamera.Count; i++)
        {
            UnityCameraParameters p = new UnityCameraParameters();
            var cam = useCamera[i];
            p.viewportSize = new Vector2(cam.pixelWidth, cam.pixelHeight);
            p.location = cam.transform.position;
            p.rotation = cam.transform.localRotation;
            p.fieldOfViewDegrees = cam.fieldOfView;
            cameraParaList.Add(p);
        }
        return cameraParaList;
    }
}
